Writing an analyzer for both console and editor
With a little orchestration it is possible to easily write two analyzer functions that share a common implementation.
open FSharp.Analyzers.SDK
open FSharp.Analyzers.SDK.ASTCollecting
open FSharp.Compiler.CodeAnalysis
open FSharp.Compiler.Text
open FSharp.Compiler.Syntax
/// This analyzer function will try and detect if any `System.*` open statement was found after any non System open.
/// See https://learn.microsoft.com/en-us/dotnet/fsharp/style-guide/conventions#sort-open-statements-topologically
/// Note that this implementation is not complete and only serves as an illustration.
/// Nested modules are not taken into account.
let private topologicallySortedOpenStatementsAnalyzer
(sourceText: ISourceText)
(untypedTree: ParsedInput)
(checkResults: FSharpCheckFileResults)
: Async<Message list>
=
async {
let allOpenStatements =
let allOpenStatements = ResizeArray<string list * range>()
let (|LongIdentAsString|) (lid: SynLongIdent) =
lid.LongIdent |> List.map (fun ident -> ident.idText)
let walker =
{ new SyntaxCollectorBase() with
override _.WalkSynModuleSigDecl(_, decl: SynModuleSigDecl) =
match decl with
| SynModuleSigDecl.Open(
target = SynOpenDeclTarget.ModuleOrNamespace(longId = LongIdentAsString value; range = mOpen)) ->
allOpenStatements.Add(value, mOpen)
| _ -> ()
override _.WalkSynModuleDecl(_, decl: SynModuleDecl) =
match decl with
| SynModuleDecl.Open(
target = SynOpenDeclTarget.ModuleOrNamespace(longId = LongIdentAsString value; range = mOpen)) ->
allOpenStatements.Add(value, mOpen)
| _ -> ()
}
ASTCollecting.walkAst walker untypedTree
allOpenStatements |> Seq.toList
let isSystemOpenStatement (openStatement: string list, mOpen: range) =
let isFromBCL () =
let line = sourceText.GetLineString(mOpen.EndLine - 1)
match checkResults.GetSymbolUseAtLocation(mOpen.EndLine, mOpen.EndColumn, line, openStatement) with
| Some symbolUse ->
match symbolUse.Symbol.Assembly.FileName with
| None -> false
| Some assemblyPath ->
// This might not be an airtight check
assemblyPath.ToLower().Contains "microsoft.netcore.app.ref"
| _ -> false
openStatement.[0].StartsWith("System") && isFromBCL ()
let nonSystemOpens = allOpenStatements |> List.skipWhile isSystemOpenStatement
return
nonSystemOpens
|> List.filter isSystemOpenStatement
|> List.map (fun (openStatement, mOpen) ->
let openStatementText = openStatement |> String.concat "."
{
Type = "Unsorted System open statement"
Message = $"%s{openStatementText} was found after non System namespaces where opened!"
Code = "SOT001"
Severity = Severity.Warning
Range = mOpen
Fixes = []
}
)
}
[<CliAnalyzer "Topologically sorted open statements">]
let cliAnalyzer (ctx: CliContext) : Async<Message list> =
topologicallySortedOpenStatementsAnalyzer ctx.SourceText ctx.ParseFileResults.ParseTree ctx.CheckFileResults
[<EditorAnalyzer "Topologically sorted open statements">]
let editorAnalyzer (ctx: EditorContext) : Async<Message list> =
match ctx.CheckFileResults with
// The editor might not have any check results for a given file. So we don't return any messages.
| None -> async.Return []
| Some checkResults ->
topologicallySortedOpenStatementsAnalyzer ctx.SourceText ctx.ParseFileResults.ParseTree checkResults
Both analyzers will follow the same code path: the console application will always have the required data, while the editor needs to be more careful.
⚠️ Please do not be tempted by calling .Value
on the EditorContext
😉.
To enable a wide range of analyzers, both context types give access to very detailed information about the source code.
Among this information is the full untyped abstract syntax tree (AST) and the typed abstract syntax tree (TAST).
As you can deduce from the example above, processing these trees is a very common task in an analyzer. But writing your own tree traversal code can be daunting and can also get quite repetitive over many analyzers.
That's why the SDK offers the ASTCollecting
and TASTCollecting
modules. In there, you'll find facility types and functions to make your analyzers author life easier.
For both trees, a type is defined, SyntaxCollectorBase and TypedTreeCollectorBase respectively,
with members you can override to have easy access to the tree elements you want to process.
Just pass an instance with your overriden members to the walkAst
or walkTast
function.
The open-statement analyzer from above uses the AST for it's analysis.
Because we want to process the SynModuleSigDecl
and SynModuleDecl
elements of the AST, we just override the two appropriate members of the SyntaxCollectorBase
type
in an object expression and pass the instance to walkAst
.
Much simpler and shorter than doing the traversal ourselves.
namespace FSharp
--------------------
namespace Microsoft.FSharp
This analyzer function will try and detect if any `System.*` open statement was found after any non System open.
See https://learn.microsoft.com/en-us/dotnet/fsharp/style-guide/conventions#sort-open-statements-topologically
Note that this implementation is not complete and only serves as an illustration.
Nested modules are not taken into account.
module ParsedInput from FSharp.Compiler.Syntax
--------------------
type ParsedInput = | ImplFile of ParsedImplFileInput | SigFile of ParsedSigFileInput member FileName: string member Identifiers: Set<string> member IsImplFile: bool member IsSigFile: bool member QualifiedName: QualifiedNameOfFile member Range: range member ScopedPragmas: ScopedPragma list
type Async = static member AsBeginEnd: computation: ('Arg -> Async<'T>) -> ('Arg * AsyncCallback * obj -> IAsyncResult) * (IAsyncResult -> 'T) * (IAsyncResult -> unit) static member AwaitEvent: event: IEvent<'Del,'T> * ?cancelAction: (unit -> unit) -> Async<'T> (requires delegate and 'Del :> Delegate) static member AwaitIAsyncResult: iar: IAsyncResult * ?millisecondsTimeout: int -> Async<bool> static member AwaitTask: task: Task<'T> -> Async<'T> + 1 overload static member AwaitWaitHandle: waitHandle: WaitHandle * ?millisecondsTimeout: int -> Async<bool> static member CancelDefaultToken: unit -> unit static member Catch: computation: Async<'T> -> Async<Choice<'T,exn>> static member Choice: computations: Async<'T option> seq -> Async<'T option> static member FromBeginEnd: beginAction: (AsyncCallback * obj -> IAsyncResult) * endAction: (IAsyncResult -> 'T) * ?cancelAction: (unit -> unit) -> Async<'T> + 3 overloads static member FromContinuations: callback: (('T -> unit) * (exn -> unit) * (OperationCanceledException -> unit) -> unit) -> Async<'T> ...
--------------------
type Async<'T>
val string: value: 'T -> string
--------------------
type string = System.String
union case SynLongIdent.SynLongIdent: id: LongIdent * dotRanges: range list * trivia: FSharp.Compiler.SyntaxTrivia.IdentTrivia option list -> SynLongIdent
--------------------
type SynLongIdent = | SynLongIdent of id: LongIdent * dotRanges: range list * trivia: IdentTrivia option list member Dots: range list member IdentsWithTrivia: SynIdent list member LongIdent: LongIdent member Range: range member RangeWithoutAnyExtraDot: range member ThereIsAnExtraDotAtTheEnd: bool member Trivia: IdentTrivia list
module List from Microsoft.FSharp.Collections
--------------------
type List<'T> = | op_Nil | op_ColonColon of Head: 'T * Tail: 'T list interface IReadOnlyList<'T> interface IReadOnlyCollection<'T> interface IEnumerable interface IEnumerable<'T> member GetReverseIndex: rank: int * offset: int -> int member GetSlice: startIndex: int option * endIndex: int option -> 'T list static member Cons: head: 'T * tail: 'T list -> 'T list member Head: 'T member IsEmpty: bool member Item: index: int -> 'T with get ...
type SyntaxCollectorBase = new: unit -> SyntaxCollectorBase abstract WalkAttribute: path: SyntaxVisitorPath * attribute: SynAttribute -> unit + 1 overload abstract WalkBinding: path: SyntaxVisitorPath * binding: SynBinding -> unit + 1 overload abstract WalkClause: path: SyntaxVisitorPath * matchClause: SynMatchClause -> unit + 1 overload abstract WalkComponentInfo: path: SyntaxVisitorPath * componentInfo: SynComponentInfo -> unit + 1 overload abstract WalkEnumCase: path: SyntaxVisitorPath * enumCase: SynEnumCase -> unit + 1 overload abstract WalkExpr: path: SyntaxVisitorPath * expr: SynExpr -> unit + 1 overload abstract WalkField: path: SyntaxVisitorPath * field: SynField -> unit + 1 overload abstract WalkInterfaceImpl: path: SyntaxVisitorPath * interfaceImpl: SynInterfaceImpl -> unit + 1 overload abstract WalkInterpolatedStringPart: path: SyntaxVisitorPath * interpolatedStringPart: SynInterpolatedStringPart -> unit + 1 overload ...
<summary> The members of this type are called by walkAst. By overwriting the members for various syntax elements, a custom operation can be executed for them. </summary>
--------------------
new: unit -> SyntaxCollectorBase
<summary> Traverses the whole AST and calls the appropriate members of the given SyntaxCollectorBase to process the syntax elements. </summary>
System.String.ToLower(culture: System.Globalization.CultureInfo) : string
module Range from FSharp.Compiler.Text
--------------------
[<Struct>] type Range = member End: pos member EndColumn: int member EndLine: int member EndRange: range member FileName: string member IsSynthetic: bool member Start: pos member StartColumn: int member StartLine: int member StartRange: range ...
type CliAnalyzerAttribute = inherit AnalyzerAttribute new: [<Optional; DefaultParameterValue (("Analyzer" :> obj))>] name: string * [<Optional; DefaultParameterValue (("" :> obj))>] shortDescription: string * [<Optional; DefaultParameterValue (("" :> obj))>] helpUri: string -> CliAnalyzerAttribute member Name: string
<summary> Marks an analyzer for scanning during the console application run. </summary>
--------------------
new: [<System.Runtime.InteropServices.Optional; System.Runtime.InteropServices.DefaultParameterValue (("Analyzer" :> obj))>] name: string * [<System.Runtime.InteropServices.Optional; System.Runtime.InteropServices.DefaultParameterValue (("" :> obj))>] shortDescription: string * [<System.Runtime.InteropServices.Optional; System.Runtime.InteropServices.DefaultParameterValue (("" :> obj))>] helpUri: string -> CliAnalyzerAttribute
<summary> All the relevant compiler information for a given file. Contains the source text, untyped and typed tree information. </summary>
<summary> Source of the current file. See <a href="https://fsharp.github.io/fsharp-compiler-docs/reference/fsharp-compiler-text-isourcetext.html">ISourceText Type</a> </summary>
<summary> Represents the results of parsing an F# file and a set of analysis operations based on the parse tree alone. See <a href="https://fsharp.github.io/fsharp-compiler-docs/reference/fsharp-compiler-codeanalysis-fsharpparsefileresults.html">FSharpParseFileResults Type</a> </summary>
<summary> A handle to the results of CheckFileInProject. See <a href="https://fsharp.github.io/fsharp-compiler-docs/reference/fsharp-compiler-codeanalysis-fsharpcheckfileresults.html">FSharpCheckFileResults Type</a> </summary>
type EditorAnalyzerAttribute = inherit AnalyzerAttribute new: [<Optional; DefaultParameterValue (("Analyzer" :> obj))>] name: string * [<Optional; DefaultParameterValue (("" :> obj))>] shortDescription: string * [<Optional; DefaultParameterValue (("" :> obj))>] helpUri: string -> EditorAnalyzerAttribute member Name: string
<summary> Marks an analyzer for scanning during IDE integration. </summary>
--------------------
new: [<System.Runtime.InteropServices.Optional; System.Runtime.InteropServices.DefaultParameterValue (("Analyzer" :> obj))>] name: string * [<System.Runtime.InteropServices.Optional; System.Runtime.InteropServices.DefaultParameterValue (("" :> obj))>] shortDescription: string * [<System.Runtime.InteropServices.Optional; System.Runtime.InteropServices.DefaultParameterValue (("" :> obj))>] helpUri: string -> EditorAnalyzerAttribute
<summary> Optional compiler information for a given file. The available contents is controlled based on what information the IDE has available. </summary>
<summary> A handle to the results of CheckFileInProject. See <a href="https://fsharp.github.io/fsharp-compiler-docs/reference/fsharp-compiler-codeanalysis-fsharpcheckfileresults.html">FSharpCheckFileResults Type</a> </summary>
<summary> Source of the current file. See <a href="https://fsharp.github.io/fsharp-compiler-docs/reference/fsharp-compiler-text-isourcetext.html">ISourceText Type</a> </summary>
<summary> Represents the results of parsing an F# file and a set of analysis operations based on the parse tree alone. See <a href="https://fsharp.github.io/fsharp-compiler-docs/reference/fsharp-compiler-codeanalysis-fsharpparsefileresults.html">FSharpParseFileResults Type</a> </summary>